package pres.seanlang.utility;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 *  URLConnection网络请求包装工具类
 *      支持http/https
 *
 * @author SeanLang
 */
public final class HttpUtils {

    /** Http get请求 */
    public final static String GET  = "GET";

    /** http post请求 */
    public final static String POST = "POST";

    /**
     *  请求响应
     */
    public interface Response extends Closeable {
        /**
         *  请求响应的状态码。
         * @return
         */
        int getCode();

        /**
         *  获取所有的响应头信息
         * @return
         */
        Map<String, String> getHeaders();

        /**
         *  获取指定的响应头信息
         * @param name
         * @return
         */
        String getHeader(String name);

        /**
         *  返回响应头content-length的值
         * @return url请求响应资源内容的长度, 如果内容长度未知返回-1.
         */
        long getContentLength();

        /**
         *  获取响应头信息中content-type的值
         * @return URL引用的资源的内容类型，如果不知道返回null。
         */
        String getContentType();

        /**
         *  获取响应头信息中content-encoding的值
         * @return URL引用的资源的内容编码，如果不知道返回null。
         */
        String getContentEncoding();

        /**
         *  服务器返回数据的stream
         * @return  可能会返回null
         */
        InputStream getContentStream() throws IOException;

        /**
         *  释放response的资源
         */
        void release();
    }

    /**
     *  异步回调
     */
    public interface Callback {
        /**
         *  请求异常回调方法
         * @param e
         */
        void onFailure(IOException e);

        /**
         *  请求响应
         * @param res  注意不要对此实例进行缓存, 跳出方法之后该实例将会失效。
         */
        void onResponse(Response res);
    }

    /**
     *  请求包装
     */
    public interface Request {
        /**
         *  设置请求的方式
         * @param method 方式参数{@link #GET},{@link #POST}
         * @return
         */
        Request setMethod(String method);

        /**
         *  设置超时时间
         * @param ms    单位毫秒
         * @return
         */
        Request setTimeout(int ms);

        /**
         *  添加请求头
         * @param key
         * @param value
         * @return
         */
        Request addHeader(String key, String value);

        /**
         *  设置当前https所用到的SSLSocketFactory.
         * @param factory
         * @return
         */
        Request setSSLSocketFactory(SSLSocketFactory factory);

        /**
         *  设置当前https所用到的HostnameVerifier
         * @param verifier
         * @return
         */
        Request setHostnameVerifier(HostnameVerifier verifier);

        /**
         *  设置连接成功之后提交的内容
         * @param mimeType      MINE类型, 如:application/json, image/png等
         * @param content       提交的内容数据
         * @return
         */
        Request setPostContent(String mimeType, byte[] content);

        /**
         *  设置提交的文本内容
         * @param mimeType      MINE类型, 如:application/json, image/png等
         * @param content       文本内容
         * @param charsetName   文本内容所使用的字符集, 如果传null或者””则表示使用utf-8
         * @return
         */
        Request setPostContent(String mimeType, String content, String charsetName);

        /**
         * 设置提交json数据
         * @param content
         * @return
         */
        Request setPostJsonContent(String content);

        /**
         *  执行请求.
         */
        Response exec() throws IOException;

        /**
         *  异步执行请求
         * @param callback  //请求结果callback, 不能传null，
         */
        void exec(Callback callback);
    }

    private static class SSLVerifiter implements X509TrustManager, HostnameVerifier {
        private boolean isUseSSLProtocol = false;

        private HostnameVerifier mHostnameVerifier;
        private SSLSocketFactory mSSLSocketFactory;

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            //X509TrustManager的接口方法
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            //X509TrustManager的接口方法
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            if(isUseSSLProtocol) {
                return new X509Certificate[0];
            } else {
                return null;
            }
        }

        @Override
        public boolean verify(String hostname, SSLSession session) {
            return !StringUtils.isEmpty(hostname);
        }

        private SSLVerifiter() {


            setHostnameVerifier(null);
        }

        public void setSocketFactory(SSLSocketFactory socketFactory) {
            if(socketFactory != null) {
                this.mSSLSocketFactory = socketFactory;
            } else {
                SSLContext context = null;         //https中ssl的context
                //获取ssl context实例
                String[] protocols = {"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "TLS", "SSLv3", "SSL"};
                for (String protocol : protocols) {
                    try {
                        context = SSLContext.getInstance(protocol);
                        if("SSLv3".equals(protocol) || "SSL".equals(protocol)) { isUseSSLProtocol = true; }
                        System.out.println("[HttpUtils-Security] SSLContext use [" + protocol + "] protocol.");
                        break;
                    } catch (NoSuchAlgorithmException e) {
                        //System.err.println("[HttpUtils-HttpRequest] Create [" + protocol + "] ssl context failed.");
                    }
                }

                if (context == null) {
                    System.err.println("[HttpUtils-Security] SSLContext is null.");
                    return;
                }

                try {
                    context.init(null, new TrustManager[]{ this }, new SecureRandom());
                } catch (KeyManagementException e) {
                    System.err.println("[HttpUtils-HttpRequest] Init ssl context failed. " + e);
                }

                mSSLSocketFactory = context.getSocketFactory();
            }
        }

        public void setHostnameVerifier(HostnameVerifier verifier) {
            if(verifier != null) {
                this.mHostnameVerifier = verifier;
            } else {
                this.mHostnameVerifier = this;
            }
        }

        private static SSLVerifiter mSSLVerifiter = null;

        static synchronized SSLVerifiter get() {
            if(mSSLVerifiter == null) {
                mSSLVerifiter = new SSLVerifiter();
            }

            return mSSLVerifiter;
        }
    }

    private static class HttpResponse implements Response {
        private HttpURLConnection connection;
        private volatile boolean isConnecting = true;    //url是否连接中.
        private long callTime;

        /**
         *  释放Response资源
         */
        public void release() {
            if(isConnecting) {
                connection.disconnect();
                isConnecting = false;
            }
        }

        private HttpResponse(HttpURLConnection connection) {
            this.connection = connection;
            this.callTime = System.currentTimeMillis();

            //启动线程检测并自动关闭connection
            ThreadUtils.execute(()->{
                ThreadUtils.sleep(5000);    //检测之前提供5秒的缓冲时间
                while (isConnecting) {
                    ThreadUtils.sleep(2000);
                    if((System.currentTimeMillis() - callTime) > 3000) {
                        release();
                    }
                }
            });
        }

        @Override
        public int getCode() {
            callTime = System.currentTimeMillis();

            try{
                return connection.getResponseCode();
            } catch (IOException e) {
                System.err.println("[HttpUtils-HttpResponse] getCode error. " + e);
                e.printStackTrace();
            }

            return -1;
        }

        @Override
        public Map<String, String> getHeaders() {
            callTime = System.currentTimeMillis();

            Map<String, List<String>> headerFields = connection.getHeaderFields();
            LinkedHashMap<String, String> headers = new LinkedHashMap<>(headerFields.size());
            for(Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
                String name = entry.getKey();
                if(StringUtils.isTrimEmpty(name)) { continue; }
                List<String> values = entry.getValue();
                if(values.size() > 0) { headers.put(name, values.get(0)); }
            }
            return headers;
        }

        @Override
        public String getHeader(String name) {
            callTime = System.currentTimeMillis();
            return connection.getHeaderField(name);
        }

        @Override
        public long getContentLength() {
            callTime = System.currentTimeMillis();
            return connection.getContentLength();
        }

        @Override
        public String getContentType() {
            callTime = System.currentTimeMillis();
            return connection.getContentType();
        }

        @Override
        public String getContentEncoding() {
            callTime = System.currentTimeMillis();
            return connection.getContentEncoding();
        }

        @Override
        public InputStream getContentStream() throws IOException {
            callTime = System.currentTimeMillis();
            return connection.getInputStream();
        }

        /**
         *  关闭Http连接.并释放相关的资源.
         *      和{@link #release()}功能是一样的，推荐使用{@link #release()}
         * @throws IOException
         */
        @Override
        public void close() throws IOException {
            release();
        }
    }

    private static class HttpRequest implements Request {
        private HttpURLConnection urlConnection;    //HttpConnection实例, 与HttpsConnection只能同时存在一种.
        private byte[] content;
        private SSLSocketFactory sslFactory;
        private HostnameVerifier sslVerifier;

        private HttpRequest() {
            sslFactory = SSLVerifiter.get().mSSLSocketFactory;
            sslVerifier = SSLVerifiter.get().mHostnameVerifier;
        }

        private void init(URL url) throws IOException {
            if(urlConnection != null) {
                urlConnection.disconnect();
                urlConnection = null;
            }
//            System.out.println("[HttpUtils-HttpRequest] url=" + url.toString());
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod(GET);        //default get request.
            urlConnection.setDoInput(true);
            //urlConnection.setDoOutput(true);
            urlConnection.setConnectTimeout(1000 * 60 * 3);      //default time 3min
            urlConnection.setReadTimeout(1000 * 60 * 3);         //default time 3min
            //connection.setChunkedStreamingMode(0);
            urlConnection.setInstanceFollowRedirects(true);
            urlConnection.setAllowUserInteraction(false);
            urlConnection.setUseCaches(false/*true*/);
            urlConnection.setRequestProperty("Cache-Control", "no-cache");
        }

        @Override
        public Request setMethod(String method) {
            if(urlConnection != null) {
                try {
                    urlConnection.setRequestMethod(method);
                } catch (ProtocolException e) {
                    System.err.println("[HttpUtils-HttpRequest] setMethod error. " + e);
                    e.printStackTrace();
                }
            }

            return this;
        }

        @Override
        public Request setTimeout(int ms) {
            if(urlConnection != null) {
                urlConnection.setConnectTimeout(ms);
                urlConnection.setReadTimeout(ms);
            }
            return this;
        }

        @Override
        public Request addHeader(String key, String value) {
            if(urlConnection != null) {
                urlConnection.setRequestProperty(key, value);
            }
            return this;
        }

        @Override
        public Request setSSLSocketFactory(SSLSocketFactory factory) {
            if(factory != null) {
                this.sslFactory = factory;
            } else {
                System.err.println("[HttpUtils-HttpRequest] setSSLSocketFactory error. factory is null");
            }
            return this;
        }

        @Override
        public Request setHostnameVerifier(HostnameVerifier verifier) {
            if(verifier != null) {
                this.sslVerifier = verifier;
            } else {
                System.err.println("[HttpUtils-HttpRequest] setHostnameVerifier error. verifier is null");
            }
            return this;
        }

        @Override
        public Request setPostContent(String mimeType, byte[] content) {
            if(urlConnection != null && content != null) {
                //addHeader("Content-Type", mimeType);
                //addHeader("Content-Length", String.valueOf(content.length));
                urlConnection.setDoOutput(true);
                urlConnection.setRequestProperty("Content-Type", mimeType);
                urlConnection.setRequestProperty("Content-Length", String.valueOf(content.length));
                this.content = Arrays.copyOfRange(content, 0, content.length);
            }
            return this;
        }

        @Override
        public Request setPostContent(String mimeType, String content, String charsetName) {
            charsetName = charsetName != null ? charsetName : "utf-8";
            Charset charset = Charset.forName(charsetName);
            setPostContent(mimeType, content.getBytes(charset));
            return this;
        }

        @Override
        public Request setPostJsonContent(String content) {
            setPostContent("application/json", content, "utf-8");
            return this;
        }

        @Override
        public Response exec() throws IOException {
            if(urlConnection == null) { throw new IOException("URLConnection open error."); }

            if(urlConnection instanceof HttpsURLConnection) {
                HttpsURLConnection httpsURLConnection = (HttpsURLConnection) urlConnection;
                httpsURLConnection.setSSLSocketFactory(sslFactory);
                httpsURLConnection.setHostnameVerifier(sslVerifier);
            }

            urlConnection.connect();

            //发送content到服务器.
            if( urlConnection.getResponseCode() >= HttpURLConnection.HTTP_OK
                    && urlConnection.getResponseCode() <= 210 ) {   //响应成功.200~210
                if(content != null) {
                    OutputStream output = urlConnection.getOutputStream();
                    output.write(content);
                    output.flush();
                    output.close();
                }
            }       //不处理301

            return new HttpResponse(urlConnection);
        }

        @Override
        public void exec(Callback callback) {
            try {
                callback.onResponse(exec());
            } catch (IOException e) {
                callback.onFailure(e);
            }
        }
    }

    private HttpUtils() { throw new UnsupportedOperationException("can't instance..."); }

    /**
     *  创建request实例
     * @param url
     * @param factory
     * @param verifier
     * @return
     */
    public static Request create(String url, SSLSocketFactory factory, HostnameVerifier verifier) {
        HttpRequest request = new HttpRequest();
        try {
            request.init(new URL(url));
            request.setSSLSocketFactory(factory);
            request.setHostnameVerifier(verifier);
        } catch (IOException e) {
            System.err.println("[HttpUtils] create error. " + e);
            e.printStackTrace();
        }

        return request;
    }

    /**
     *  创建request实例
     * @param url
     * @return
     */
    public static Request create(String url) {
        return create(url, null, null);
    }

    /**
     *  执行get请求 <br>
     *      简化HttpUtils.create(xxxx).doGet()
     * @param url
     * @return
     */
    public static Response doGet(String url) throws IOException {
        Request req = create(url)
                .setMethod(GET);
        return req.exec();
    }

    /**
     *  异步执行get请求 <br>
     * @param url
     * @param callback
     */
    public static void doGet(String url, Callback callback) {
        Request req = create(url)
                .setMethod(GET);
        req.exec(callback);
    }

    /**
     *  post请求
     * @param url
     * @param json
     * @return
     * @throws IOException
     */
    public static Response doPost(String url, String json) throws IOException {
        Request req = create(url)
                .setMethod(POST)
                .setPostJsonContent(json);
        return req.exec();
    }

    /**
     *  异步post请求
     * @param url
     * @param json
     * @param callback
     */
    public static void doPost(String url, String json, Callback callback) {
        create(url)
                .setMethod(POST)
                .setPostJsonContent(json)
                .exec(callback);
        Request req = create(url)
                        .setMethod(POST)
                        .setPostJsonContent(json);
        req.exec(callback);
    }

    /**
     *  设置https请求SSL相关的SSLSocketFactory。 全局使用.
     *    如果需要恢复默认传null
     * @param factory
     */
    public static void setSSLSocketFactory(SSLSocketFactory factory) {
        SSLVerifiter.get().setSocketFactory(factory);
        HttpsURLConnection.setDefaultSSLSocketFactory(SSLVerifiter.get().mSSLSocketFactory);
    }

    /**
     *  设置https请求SSL相关的HostnameVerifier。 全局使用.
     *    如果需要恢复默认传null
     * @param verifier
     */
    public static void setHostnameVerifier(HostnameVerifier verifier) {
        SSLVerifiter.get().setHostnameVerifier(verifier);
        HttpsURLConnection.setDefaultHostnameVerifier(SSLVerifiter.get().mHostnameVerifier);

    }
}
